🎖️GitЯра🎖️
Node / meshtastic / Meshtastic-Android / files / feature / map / src / commonMain / kotlin / org / meshtastic / feature / map / component / MaplibreMapContent.kt
Displaying Raw • Download
feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MaplibreMapContent.kt b7cdc26a6e2b9076f73a20983a8ecf3c5287a784 (b7cdc26a) Text, 16.48 KB
T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.feature.map.component
Tff7b72import T7ee787androidx.compose.animation.core.LinearEasing
Tff7b72import T7ee787androidx.compose.animation.core.RepeatMode
Tff7b72import T7ee787androidx.compose.animation.core.animateFloat
Tff7b72import T7ee787androidx.compose.animation.core.infiniteRepeatable
Tff7b72import T7ee787androidx.compose.animation.core.rememberInfiniteTransition
Tff7b72import T7ee787androidx.compose.animation.core.tween
Tff7b72import T7ee787androidx.compose.material3.MaterialTheme
Tff7b72import T7ee787androidx.compose.runtime.Composable
Tff7b72import T7ee787androidx.compose.runtime.LaunchedEffect
Tff7b72import T7ee787androidx.compose.runtime.getValue
Tff7b72import T7ee787androidx.compose.runtime.mutableStateOf
Tff7b72import T7ee787androidx.compose.runtime.remember
Tff7b72import T7ee787androidx.compose.runtime.rememberCoroutineScope
Tff7b72import T7ee787androidx.compose.runtime.rememberUpdatedState
Tff7b72import T7ee787androidx.compose.runtime.setValue
Tff7b72import T7ee787androidx.compose.ui.Modifier
Tff7b72import T7ee787androidx.compose.ui.graphics.Color
Tff7b72import T7ee787androidx.compose.ui.unit.dp
Tff7b72import T7ee787androidx.compose.ui.unit.em
Tff7b72import T7ee787kotlinx.coroutines.delay
Tff7b72import T7ee787kotlinx.coroutines.launch
Tff7b72import T7ee787kotlinx.datetime.Clock
Tff7b72import T7ee787org.maplibre.compose.camera.CameraPosition
Tff7b72import T7ee787org.maplibre.compose.camera.CameraState
Tff7b72import T7ee787org.maplibre.compose.expressions.dsl.asString
Tff7b72import T7ee787org.maplibre.compose.expressions.dsl.condition
Tff7b72import T7ee787org.maplibre.compose.expressions.dsl.const
Tff7b72import T7ee787org.maplibre.compose.expressions.dsl.convertToBoolean
Tff7b72import T7ee787org.maplibre.compose.expressions.dsl.convertToColor
Tff7b72import T7ee787org.maplibre.compose.expressions.dsl.convertToNumber
Tff7b72import T7ee787org.maplibre.compose.expressions.dsl.dp
Tff7b72import T7ee787org.maplibre.compose.expressions.dsl.exponential
Tff7b72import T7ee787org.maplibre.compose.expressions.dsl.feature
Tff7b72import T7ee787org.maplibre.compose.expressions.dsl.format
Tff7b72import T7ee787org.maplibre.compose.expressions.dsl.interpolate
Tff7b72import T7ee787org.maplibre.compose.expressions.dsl.not
Tff7b72import T7ee787org.maplibre.compose.expressions.dsl.offset
Tff7b72import T7ee787org.maplibre.compose.expressions.dsl.span
Tff7b72import T7ee787org.maplibre.compose.expressions.dsl.switch
Tff7b72import T7ee787org.maplibre.compose.expressions.dsl.times
Tff7b72import T7ee787org.maplibre.compose.expressions.dsl.zoom
Tff7b72import T7ee787org.maplibre.compose.layers.CircleLayer
Tff7b72import T7ee787org.maplibre.compose.layers.HillshadeLayer
Tff7b72import T7ee787org.maplibre.compose.layers.SymbolLayer
Tff7b72import T7ee787org.maplibre.compose.location.LocationPuck
Tff7b72import T7ee787org.maplibre.compose.location.UserLocationState
Tff7b72import T7ee787org.maplibre.compose.map.GestureOptions
Tff7b72import T7ee787org.maplibre.compose.map.MapOptions
Tff7b72import T7ee787org.maplibre.compose.map.MaplibreMap
Tff7b72import T7ee787org.maplibre.compose.map.OrnamentOptions
Tff7b72import T7ee787org.maplibre.compose.material3.LocationPuckDefaults
Tff7b72import T7ee787org.maplibre.compose.sources.GeoJsonData
Tff7b72import T7ee787org.maplibre.compose.sources.GeoJsonOptions
Tff7b72import T7ee787org.maplibre.compose.sources.RasterDemEncoding
Tff7b72import T7ee787org.maplibre.compose.sources.rememberGeoJsonSource
Tff7b72import T7ee787org.maplibre.compose.sources.rememberRasterDemSource
Tff7b72import T7ee787org.maplibre.compose.style.BaseStyle
Tff7b72import T7ee787org.maplibre.compose.style.StyleState
Tff7b72import T7ee787org.maplibre.compose.style.rememberStyleState
Tff7b72import T7ee787org.maplibre.compose.util.ClickResult
Tff7b72import T7ee787org.maplibre.spatialk.geojson.Point
Tff7b72import T7ee787org.meshtastic.core.model.DataPacket
Tff7b72import T7ee787org.meshtastic.core.model.Node
Tff7b72import T7ee787org.meshtastic.feature.map.util.MARKER_STROKE_WIDTH
Tff7b72import T7ee787org.meshtastic.feature.map.util.NODE_MARKER_RADIUS
Tff7b72import T7ee787org.meshtastic.feature.map.util.PRECISION_CIRCLE_STROKE_ALPHA
Tff7b72import T7ee787org.meshtastic.feature.map.util.nodesToFeatureCollection
Tff7b72import T7ee787org.meshtastic.feature.map.util.waypointsToFeatureCollection
Tff7b72import T7ee787org.maplibre.spatialk.geojson.Position Tff7b72as Te6edf3GeoPosition
Tff7b72private Tff7b72val Te6edf3OnlineStrokeColor Tff7b72= Te6edf3ColorTb4b4b4(T79c0ff0Te6edf3xFF4CAF50Tb4b4b4) T8b949e// Green — node heard within online threshold
Tff7b72private Tff7b72val Te6edf3OfflineStrokeColor Tff7b72= Te6edf3ColorTb4b4b4(T79c0ff0Te6edf3xFF9E9E9ETb4b4b4) T8b949e// Gray — node not heard recently
Tff7b72private Tff7b72const Tff7b72val Te6edf3CLUSTER_RADIUS Tff7b72= T79c0ff5T79c0ff0
Tff7b72private Tff7b72const Tff7b72val Te6edf3CLUSTER_MIN_POINTS Tff7b72= T79c0ff1T79c0ff0
Tff7b72private Tff7b72const Tff7b72val Te6edf3PRECISION_CIRCLE_FILL_ALPHA Tff7b72= T79c0ff0.1f
T8b949e/**
* Ground resolution at the equator: meters per pixel = 156543.03 / 2^zoom. We use an exponential(2) interpolation with
* two stops to compute the conversion factor from meters to pixels at each zoom level. The result is multiplied by the
* per-feature `precision_meters` property to produce a screen-pixel radius.
*/
Tff7b72private Tff7b72const Tff7b72val Te6edf3EQUATORIAL_METERS_PER_PIXEL_ZOOM0 Tff7b72= T79c0ff1T79c0ff5T79c0ff6T79c0ff5T79c0ff4T79c0ff3.03f
Tff7b72private Tff7b72const Tff7b72val Te6edf3PRECISION_ZOOM_MIN Tff7b72= T79c0ff0
Tff7b72private Tff7b72const Tff7b72val Te6edf3PRECISION_ZOOM_MAX Tff7b72= T79c0ff2T79c0ff4
Tff7b72private Tff7b72const Tff7b72val Te6edf3PRECISION_SCALE_MIN Tff7b72= T79c0ff1f Tff7b72/ Te6edf3EQUATORIAL_METERS_PER_PIXEL_ZOOM0
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffMagicNumberTa5d6ff"Tb4b4b4)
Tff7b72private Tff7b72const Tff7b72val Te6edf3PRECISION_SCALE_MAX Tff7b72= T79c0ff1T79c0ff6Te6edf3_777_216f Tff7b72/ Te6edf3EQUATORIAL_METERS_PER_PIXEL_ZOOM0 T8b949e// 2^24
Tff7b72private Tff7b72const Tff7b72val Te6edf3CLUSTER_OPACITY Tff7b72= T79c0ff0.85f
Tff7b72private Tff7b72const Tff7b72val Te6edf3LABEL_OFFSET_EM Tff7b72= T79c0ff1.5f
Tff7b72private Tff7b72const Tff7b72val Te6edf3CLUSTER_ZOOM_INCREMENT Tff7b72= T79c0ff2.0
Tff7b72private Tff7b72const Tff7b72val Te6edf3HILLSHADE_EXAGGERATION Tff7b72= T79c0ff0.5f
Tff7b72private Tff7b72const Tff7b72val Te6edf3PULSE_DURATION_MS Tff7b72= T79c0ff1T79c0ff5T79c0ff0T79c0ff0
Tff7b72private Tff7b72const Tff7b72val Te6edf3PULSE_MAX_RADIUS_DP Tff7b72= T79c0ff1T79c0ff4f
Tff7b72private Tff7b72const Tff7b72val Te6edf3PULSE_START_OPACITY Tff7b72= T79c0ff0.5f
Tff7b72private Tff7b72const Tff7b72val Te6edf3PULSE_WINDOW_SECONDS Tff7b72= T79c0ff5L
Tff7b72private Tff7b72const Tff7b72val Te6edf3PULSE_TICK_INTERVAL_MS Tff7b72= T79c0ff1T79c0ff0T79c0ff0T79c0ff0L
T8b949e/** Free Terrain Tiles (Terrarium encoding) hosted on AWS. No API key required. */
Tff7b72private Tff7b72val Te6edf3TERRAIN_TILES Tff7b72= Te6edf3listOfTb4b4b4(Ta5d6ff"Ta5d6ffhttps://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.pngTa5d6ff"Tb4b4b4)
T8b949e/**
* Main map content composable using MapLibre Compose Multiplatform.
*
* Renders nodes as clustered markers, waypoints, and optional overlays (position tracks, traceroute routes). Replaces
* both the Google Maps and OSMDroid implementations with a single cross-platform composable.
*/
Tf0883e@Composable
Tff7b72fun Td2a8ffMaplibreMapContentTb4b4b4(
Te6edf3nodesTb4b4b4: Te6edf3ListTff7b72<Te6edf3NodeTff7b72>Tb4b4b4,
Te6edf3waypointsTb4b4b4: Te6edf3MapTff7b72<Tffa657IntTb4b4b4, Te6edf3DataPacketTff7b72>Tb4b4b4,
Te6edf3baseStyleTb4b4b4: Te6edf3BaseStyleTb4b4b4,
Te6edf3cameraStateTb4b4b4: Te6edf3CameraStateTb4b4b4,
Te6edf3myNodeNumTb4b4b4: Tffa657Int?Tb4b4b4,
Te6edf3showWaypointsTb4b4b4: Tffa657BooleanTb4b4b4,
Te6edf3showPrecisionCircleTb4b4b4: Tffa657BooleanTb4b4b4,
Te6edf3showHillshadeTb4b4b4: Tffa657BooleanTb4b4b4,
Te6edf3onNodeClickTb4b4b4: Tb4b4b4(Tffa657IntTb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4,
Te6edf3onMapLongClickTb4b4b4: Tb4b4b4(Te6edf3GeoPositionTb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4,
Te6edf3modifierTb4b4b4: Te6edf3Modifier Tff7b72= Te6edf3ModifierTb4b4b4,
Te6edf3gestureOptionsTb4b4b4: Te6edf3GestureOptions Tff7b72= Te6edf3GestureOptionsTb4b4b4.Te6edf3StandardTb4b4b4,
Te6edf3styleStateTb4b4b4: Te6edf3StyleState Tff7b72= Te6edf3rememberStyleStateTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3onCameraMoveTb4b4b4: Tb4b4b4(Te6edf3CameraPositionTb4b4b4) Tff7b72-Tff7b72> Tffa657Unit Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Te6edf3onWaypointClickTb4b4b4: Tb4b4b4(Tffa657IntTb4b4b4) Tff7b72-Tff7b72> Tffa657Unit Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Te6edf3onMapLoadTb4b4b4: Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tffa657Unit Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Te6edf3onMapLoadFailTb4b4b4: Tb4b4b4(Tffa657String?Tb4b4b4) Tff7b72-Tff7b72> Tffa657Unit Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Te6edf3locationStateTb4b4b4: Te6edf3UserLocationState? Tff7b72= Tff7b72nullTb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3MaplibreMapTb4b4b4(
Te6edf3modifier Tff7b72= Te6edf3modifierTb4b4b4,
Te6edf3baseStyle Tff7b72= Te6edf3baseStyleTb4b4b4,
Te6edf3cameraState Tff7b72= Te6edf3cameraStateTb4b4b4,
Te6edf3styleState Tff7b72= Te6edf3styleStateTb4b4b4,
Te6edf3options Tff7b72= Te6edf3MapOptionsTb4b4b4(Te6edf3gestureOptions Tff7b72= Te6edf3gestureOptionsTb4b4b4, Te6edf3ornamentOptions Tff7b72= Te6edf3OrnamentOptionsTb4b4b4.Te6edf3OnlyLogoTb4b4b4)Tb4b4b4,
Te6edf3onMapLongClick Tff7b72= Tb4b4b4{ Te6edf3positionTb4b4b4, Te6edf3_ Tff7b72-Tff7b72>
Te6edf3onMapLongClickTb4b4b4(Te6edf3positionTb4b4b4)
Te6edf3ClickResultTb4b4b4.Te6edf3Consume
Tb4b4b4}Tb4b4b4,
Te6edf3onMapLoadFinished Tff7b72= Te6edf3onMapLoadTb4b4b4,
Te6edf3onMapLoadFailed Tff7b72= Te6edf3onMapLoadFailTb4b4b4,
Tb4b4b4) Tb4b4b4{
T8b949e// --- Terrain hillshade overlay ---
Tff7b72if Tb4b4b4(Te6edf3showHillshadeTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3demSource Tff7b72= Te6edf3rememberRasterDemSourceTb4b4b4(Te6edf3tiles Tff7b72= Te6edf3TERRAIN_TILESTb4b4b4, Te6edf3encoding Tff7b72= Te6edf3RasterDemEncodingTb4b4b4.Te6edf3TerrariumTb4b4b4)
Te6edf3HillshadeLayerTb4b4b4(Te6edf3id Tff7b72= Ta5d6ff"Ta5d6ffterrain-hillshadeTa5d6ff"Tb4b4b4, Te6edf3source Tff7b72= Te6edf3demSourceTb4b4b4, Te6edf3exaggeration Tff7b72= Te6edf3constTb4b4b4(Te6edf3HILLSHADE_EXAGGERATIONTb4b4b4)Tb4b4b4)
Tb4b4b4}
T8b949e// --- Node markers with clustering ---
Te6edf3NodeMarkerLayersTb4b4b4(
Te6edf3nodes Tff7b72= Te6edf3nodesTb4b4b4,
Te6edf3myNodeNum Tff7b72= Te6edf3myNodeNumTb4b4b4,
Te6edf3showPrecisionCircle Tff7b72= Te6edf3showPrecisionCircleTb4b4b4,
Te6edf3cameraState Tff7b72= Te6edf3cameraStateTb4b4b4,
Te6edf3onNodeClick Tff7b72= Te6edf3onNodeClickTb4b4b4,
Tb4b4b4)
T8b949e// --- Waypoint markers ---
Tff7b72if Tb4b4b4(Te6edf3showWaypointsTb4b4b4) Tb4b4b4{
Te6edf3WaypointMarkerLayersTb4b4b4(Te6edf3waypoints Tff7b72= Te6edf3waypointsTb4b4b4, Te6edf3onWaypointClick Tff7b72= Te6edf3onWaypointClickTb4b4b4)
Tb4b4b4}
T8b949e// --- User location puck ---
Tff7b72if Tb4b4b4(Te6edf3locationState Tff7b72!Tff7b72= Tff7b72nullTb4b4b4) Tb4b4b4{
Te6edf3LocationPuckTb4b4b4(
Te6edf3idPrefix Tff7b72= Ta5d6ff"Ta5d6ffuser-locationTa5d6ff"Tb4b4b4,
Te6edf3locationState Tff7b72= Te6edf3locationStateTb4b4b4,
Te6edf3cameraState Tff7b72= Te6edf3cameraStateTb4b4b4,
Te6edf3colors Tff7b72= Te6edf3LocationPuckDefaultsTb4b4b4.Te6edf3colorsTb4b4b4(Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
T8b949e// Persist camera position when it stops moving
Tff7b72val Te6edf3currentOnCameraMove Tff7b72= Te6edf3rememberUpdatedStateTb4b4b4(Te6edf3onCameraMoveTb4b4b4)
Te6edf3LaunchedEffectTb4b4b4(Te6edf3cameraStateTb4b4b4.Te6edf3isCameraMovingTb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3cameraStateTb4b4b4.Te6edf3isCameraMovingTb4b4b4) Tb4b4b4{
Te6edf3currentOnCameraMoveTb4b4b4.Te6edf3valueTb4b4b4(Te6edf3cameraStateTb4b4b4.Te6edf3positionTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
T8b949e/** Node markers rendered as clustered circles with per-node colors and short name labels. */
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffLongMethodTa5d6ff"Tb4b4b4)
Tf0883e@Composable
Tff7b72private Tff7b72fun Td2a8ffNodeMarkerLayersTb4b4b4(
Te6edf3nodesTb4b4b4: Te6edf3ListTff7b72<Te6edf3NodeTff7b72>Tb4b4b4,
Te6edf3myNodeNumTb4b4b4: Tffa657Int?Tb4b4b4,
Te6edf3showPrecisionCircleTb4b4b4: Tffa657BooleanTb4b4b4,
Te6edf3cameraStateTb4b4b4: Te6edf3CameraStateTb4b4b4,
Te6edf3onNodeClickTb4b4b4: Tb4b4b4(Tffa657IntTb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4,
Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3coroutineScope Tff7b72= Te6edf3rememberCoroutineScopeTb4b4b4(Tb4b4b4)
T8b949e// Tick current time to expire pulse animations after PULSE_WINDOW_SECONDS
Tff7b72var Te6edf3nowEpochSeconds Tff7b72by Te6edf3remember Tb4b4b4{ Te6edf3mutableStateOfTb4b4b4(Te6edf3ClockTb4b4b4.Te6edf3SystemTb4b4b4.Te6edf3nowTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3epochSecondsTb4b4b4) Tb4b4b4}
Te6edf3LaunchedEffectTb4b4b4(Tffa657UnitTb4b4b4) Tb4b4b4{
Tff7b72while Tb4b4b4(Tff7b72trueTb4b4b4) Tb4b4b4{
Te6edf3delayTb4b4b4(Te6edf3PULSE_TICK_INTERVAL_MSTb4b4b4)
Te6edf3nowEpochSeconds Tff7b72= Te6edf3ClockTb4b4b4.Te6edf3SystemTb4b4b4.Te6edf3nowTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3epochSeconds
Tb4b4b4}
Tb4b4b4}
Tff7b72val Te6edf3featureCollection Tff7b72=
Te6edf3rememberTb4b4b4(Te6edf3nodesTb4b4b4, Te6edf3myNodeNumTb4b4b4, Te6edf3nowEpochSecondsTb4b4b4) Tb4b4b4{ Te6edf3nodesToFeatureCollectionTb4b4b4(Te6edf3nodesTb4b4b4, Te6edf3myNodeNumTb4b4b4, Te6edf3nowEpochSecondsTb4b4b4) Tb4b4b4}
T8b949e// Read M3 semantic colors for map layers (recomposes on theme change)
Tff7b72val Te6edf3clusterColor Tff7b72= Te6edf3MaterialThemeTb4b4b4.Te6edf3colorSchemeTb4b4b4.Te6edf3primary
Tff7b72val Te6edf3labelColor Tff7b72= Te6edf3MaterialThemeTb4b4b4.Te6edf3colorSchemeTb4b4b4.Te6edf3onSurfaceVariant
Tff7b72val Te6edf3clusterLabelColor Tff7b72= Te6edf3MaterialThemeTb4b4b4.Te6edf3colorSchemeTb4b4b4.Te6edf3onPrimary
T8b949e// Pulsing ring animation for recently-heard nodes
Tff7b72val Te6edf3pulseTransition Tff7b72= Te6edf3rememberInfiniteTransitionTb4b4b4(Te6edf3label Tff7b72= Ta5d6ff"Ta5d6ffnode-pulseTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3pulseProgress Tff7b72by
Te6edf3pulseTransitionTb4b4b4.Te6edf3animateFloatTb4b4b4(
Te6edf3initialValue Tff7b72= T79c0ff0fTb4b4b4,
Te6edf3targetValue Tff7b72= T79c0ff1fTb4b4b4,
Te6edf3animationSpec Tff7b72= Te6edf3infiniteRepeatableTb4b4b4(Te6edf3tweenTb4b4b4(Te6edf3PULSE_DURATION_MSTb4b4b4, Te6edf3easing Tff7b72= Te6edf3LinearEasingTb4b4b4)Tb4b4b4, Te6edf3RepeatModeTb4b4b4.Te6edf3RestartTb4b4b4)Tb4b4b4,
Te6edf3label Tff7b72= Ta5d6ff"Ta5d6ffpulse-progressTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tff7b72val Te6edf3nodesSource Tff7b72=
Te6edf3rememberGeoJsonSourceTb4b4b4(
Te6edf3data Tff7b72= Te6edf3GeoJsonDataTb4b4b4.Te6edf3FeaturesTb4b4b4(Te6edf3featureCollectionTb4b4b4)Tb4b4b4,
Te6edf3options Tff7b72=
Te6edf3GeoJsonOptionsTb4b4b4(Te6edf3cluster Tff7b72= Tff7b72trueTb4b4b4, Te6edf3clusterRadius Tff7b72= Te6edf3CLUSTER_RADIUSTb4b4b4, Te6edf3clusterMinPoints Tff7b72= Te6edf3CLUSTER_MIN_POINTSTb4b4b4)Tb4b4b4,
Tb4b4b4)
T8b949e// Cluster circles — tap to zoom in toward expansion
Te6edf3CircleLayerTb4b4b4(
Te6edf3id Tff7b72= Ta5d6ff"Ta5d6ffnode-clustersTa5d6ff"Tb4b4b4,
Te6edf3source Tff7b72= Te6edf3nodesSourceTb4b4b4,
Te6edf3filter Tff7b72= Te6edf3featureTb4b4b4.Te6edf3hasTb4b4b4(Ta5d6ff"Ta5d6ffclusterTa5d6ff"Tb4b4b4)Tb4b4b4,
Te6edf3radius Tff7b72= Te6edf3constTb4b4b4(T79c0ff2T79c0ff0.Te6edf3dpTb4b4b4)Tb4b4b4,
Te6edf3color Tff7b72= Te6edf3constTb4b4b4(Te6edf3clusterColorTb4b4b4)Tb4b4b4,
Te6edf3opacity Tff7b72= Te6edf3constTb4b4b4(Te6edf3CLUSTER_OPACITYTb4b4b4)Tb4b4b4,
Te6edf3strokeWidth Tff7b72= Te6edf3constTb4b4b4(Te6edf3MARKER_STROKE_WIDTHTb4b4b4)Tb4b4b4,
Te6edf3strokeColor Tff7b72= Te6edf3constTb4b4b4(Te6edf3clusterLabelColorTb4b4b4)Tb4b4b4,
Te6edf3onClick Tff7b72= Tb4b4b4{ Te6edf3features Tff7b72-Tff7b72>
Tff7b72val Te6edf3cluster Tff7b72= Te6edf3featuresTb4b4b4.Te6edf3firstOrNullTb4b4b4(Tb4b4b4) Tff7b72?: Tff7b72returnTf0883e@CircleLayer Te6edf3ClickResultTb4b4b4.Te6edf3Pass
Tff7b72val Te6edf3target Tff7b72= Tb4b4b4(Te6edf3clusterTb4b4b4.Te6edf3geometry Tff7b72as? Te6edf3PointTb4b4b4)Tff7b72?.Te6edf3coordinates Tff7b72?: Tff7b72returnTf0883e@CircleLayer Te6edf3ClickResultTb4b4b4.Te6edf3Pass
Te6edf3coroutineScopeTb4b4b4.Te6edf3launch Tb4b4b4{
Te6edf3cameraStateTb4b4b4.Te6edf3animateToTb4b4b4(
Te6edf3cameraStateTb4b4b4.Te6edf3positionTb4b4b4.Te6edf3copyTb4b4b4(
Te6edf3target Tff7b72= Te6edf3targetTb4b4b4,
Te6edf3zoom Tff7b72= Te6edf3minOfTb4b4b4(Te6edf3cameraStateTb4b4b4.Te6edf3positionTb4b4b4.Te6edf3zoom Tff7b72+ Te6edf3CLUSTER_ZOOM_INCREMENTTb4b4b4, Te6edf3PRECISION_ZOOM_MAXTb4b4b4.Te6edf3toDoubleTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Te6edf3ClickResultTb4b4b4.Te6edf3Consume
Tb4b4b4}Tb4b4b4,
Tb4b4b4)
T8b949e// Cluster count labels
Te6edf3SymbolLayerTb4b4b4(
Te6edf3id Tff7b72= Ta5d6ff"Ta5d6ffnode-cluster-countTa5d6ff"Tb4b4b4,
Te6edf3source Tff7b72= Te6edf3nodesSourceTb4b4b4,
Te6edf3filter Tff7b72= Te6edf3featureTb4b4b4.Te6edf3hasTb4b4b4(Ta5d6ff"Ta5d6ffclusterTa5d6ff"Tb4b4b4)Tb4b4b4,
Te6edf3textField Tff7b72= Te6edf3featureTff7b72[Ta5d6ff"Ta5d6ffpoint_countTa5d6ff"Tff7b72]Tb4b4b4.Te6edf3asStringTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3textColor Tff7b72= Te6edf3constTb4b4b4(Te6edf3clusterLabelColorTb4b4b4)Tb4b4b4,
Te6edf3textSize Tff7b72= Te6edf3constTb4b4b4(T79c0ff1.2fTb4b4b4.Te6edf3emTb4b4b4)Tb4b4b4,
Tb4b4b4)
T8b949e// Pulsing ring behind recently-heard nodes — indicates new packet received
Tff7b72val Te6edf3pulseRadius Tff7b72= Tb4b4b4(Te6edf3NODE_MARKER_RADIUSTb4b4b4.Te6edf3value Tff7b72+ Tb4b4b4(Te6edf3PULSE_MAX_RADIUS_DP Tff7b72- Te6edf3NODE_MARKER_RADIUSTb4b4b4.Te6edf3valueTb4b4b4) Tff7b72* Te6edf3pulseProgressTb4b4b4)Tb4b4b4.Te6edf3dp
Tff7b72val Te6edf3pulseOpacity Tff7b72= Te6edf3PULSE_START_OPACITY Tff7b72* Tb4b4b4(T79c0ff1f Tff7b72- Te6edf3pulseProgressTb4b4b4)
Te6edf3CircleLayerTb4b4b4(
Te6edf3id Tff7b72= Ta5d6ff"Ta5d6ffnode-pulse-ringTa5d6ff"Tb4b4b4,
Te6edf3source Tff7b72= Te6edf3nodesSourceTb4b4b4,
Te6edf3filter Tff7b72= Te6edf3featureTff7b72[Ta5d6ff"Ta5d6ffrecently_heardTa5d6ff"Tff7b72]Tb4b4b4.Te6edf3convertToBooleanTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3radius Tff7b72= Te6edf3constTb4b4b4(Te6edf3pulseRadiusTb4b4b4)Tb4b4b4,
Te6edf3color Tff7b72= Te6edf3constTb4b4b4(Te6edf3OnlineStrokeColorTb4b4b4)Tb4b4b4,
Te6edf3opacity Tff7b72= Te6edf3constTb4b4b4(Te6edf3pulseOpacityTb4b4b4)Tb4b4b4,
Tb4b4b4)
T8b949e// Individual node markers with per-node background color and online-status stroke
Te6edf3CircleLayerTb4b4b4(
Te6edf3id Tff7b72= Ta5d6ff"Ta5d6ffnode-markersTa5d6ff"Tb4b4b4,
Te6edf3source Tff7b72= Te6edf3nodesSourceTb4b4b4,
Te6edf3filter Tff7b72= Tff7b72!Te6edf3featureTb4b4b4.Te6edf3hasTb4b4b4(Ta5d6ff"Ta5d6ffclusterTa5d6ff"Tb4b4b4)Tb4b4b4,
Te6edf3radius Tff7b72= Te6edf3constTb4b4b4(Te6edf3NODE_MARKER_RADIUSTb4b4b4)Tb4b4b4,
Te6edf3color Tff7b72= Te6edf3featureTff7b72[Ta5d6ff"Ta5d6ffbackground_colorTa5d6ff"Tff7b72]Tb4b4b4.Te6edf3convertToColorTb4b4b4(Te6edf3constTb4b4b4(Te6edf3clusterColorTb4b4b4)Tb4b4b4)Tb4b4b4,
Te6edf3strokeWidth Tff7b72= Te6edf3constTb4b4b4(Te6edf3MARKER_STROKE_WIDTHTb4b4b4)Tb4b4b4,
Te6edf3strokeColor Tff7b72=
Te6edf3switchTb4b4b4(
Te6edf3conditionTb4b4b4(Te6edf3featureTff7b72[Ta5d6ff"Ta5d6ffis_onlineTa5d6ff"Tff7b72]Tb4b4b4.Te6edf3convertToBooleanTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3constTb4b4b4(Te6edf3OnlineStrokeColorTb4b4b4)Tb4b4b4)Tb4b4b4,
Te6edf3fallback Tff7b72= Te6edf3constTb4b4b4(Te6edf3OfflineStrokeColorTb4b4b4)Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Te6edf3onClick Tff7b72= Tb4b4b4{ Te6edf3features Tff7b72-Tff7b72>
Tff7b72val Te6edf3nodeNum Tff7b72= Te6edf3featuresTb4b4b4.Te6edf3firstOrNullTb4b4b4(Tb4b4b4)Tff7b72?.Te6edf3propertiesTff7b72?.Te6edf3getTb4b4b4(Ta5d6ff"Ta5d6ffnode_numTa5d6ff"Tb4b4b4)Tff7b72?.Te6edf3toStringTb4b4b4(Tb4b4b4)Tff7b72?.Te6edf3toIntOrNullTb4b4b4(Tb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3nodeNum Tff7b72!Tff7b72= Tff7b72nullTb4b4b4) Tb4b4b4{
Te6edf3onNodeClickTb4b4b4(Te6edf3nodeNumTb4b4b4)
Te6edf3ClickResultTb4b4b4.Te6edf3Consume
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3ClickResultTb4b4b4.Te6edf3Pass
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Tb4b4b4)
T8b949e// Short name labels with online status dot below node markers
Te6edf3SymbolLayerTb4b4b4(
Te6edf3id Tff7b72= Ta5d6ff"Ta5d6ffnode-labelsTa5d6ff"Tb4b4b4,
Te6edf3source Tff7b72= Te6edf3nodesSourceTb4b4b4,
Te6edf3filter Tff7b72= Tff7b72!Te6edf3featureTb4b4b4.Te6edf3hasTb4b4b4(Ta5d6ff"Ta5d6ffclusterTa5d6ff"Tb4b4b4)Tb4b4b4,
Te6edf3textField Tff7b72=
Te6edf3formatTb4b4b4(
Te6edf3spanTb4b4b4(Te6edf3featureTff7b72[Ta5d6ff"Ta5d6ffshort_nameTa5d6ff"Tff7b72]Tb4b4b4.Te6edf3asStringTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4,
Te6edf3spanTb4b4b4(
Te6edf3constTb4b4b4(Ta5d6ff"Ta5d6ff Ta5d6ff\Ta5d6ffu25CFTa5d6ff"Tb4b4b4)Tb4b4b4, T8b949e// U+25CF Black Circle
Te6edf3textColor Tff7b72=
Te6edf3switchTb4b4b4(
Te6edf3conditionTb4b4b4(Te6edf3featureTff7b72[Ta5d6ff"Ta5d6ffis_onlineTa5d6ff"Tff7b72]Tb4b4b4.Te6edf3convertToBooleanTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3constTb4b4b4(Te6edf3OnlineStrokeColorTb4b4b4)Tb4b4b4)Tb4b4b4,
Te6edf3fallback Tff7b72= Te6edf3constTb4b4b4(Te6edf3OfflineStrokeColorTb4b4b4)Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Te6edf3textSize Tff7b72= Te6edf3constTb4b4b4(T79c0ff0.9fTb4b4b4.Te6edf3emTb4b4b4)Tb4b4b4,
Te6edf3textOffset Tff7b72= Te6edf3offsetTb4b4b4(T79c0ff0fTb4b4b4.Te6edf3emTb4b4b4, Te6edf3LABEL_OFFSET_EMTb4b4b4.Te6edf3emTb4b4b4)Tb4b4b4,
Te6edf3textColor Tff7b72= Te6edf3constTb4b4b4(Te6edf3labelColorTb4b4b4)Tb4b4b4,
Te6edf3textAllowOverlap Tff7b72= Te6edf3constTb4b4b4(Tff7b72trueTb4b4b4)Tb4b4b4,
Te6edf3iconAllowOverlap Tff7b72= Te6edf3constTb4b4b4(Tff7b72trueTb4b4b4)Tb4b4b4,
Tb4b4b4)
T8b949e// Precision circles — sized by precision_meters property converted to screen pixels via zoom interpolation
Tff7b72if Tb4b4b4(Te6edf3showPrecisionCircleTb4b4b4) Tb4b4b4{
T8b949e// Meters-to-pixels factor doubles with each zoom level (equatorial approximation)
Tff7b72val Te6edf3metersToPixels Tff7b72=
Te6edf3interpolateTb4b4b4(
Te6edf3exponentialTb4b4b4(T79c0ff2fTb4b4b4)Tb4b4b4,
Te6edf3zoomTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3PRECISION_ZOOM_MIN Te6edf3to Te6edf3constTb4b4b4(Te6edf3PRECISION_SCALE_MINTb4b4b4)Tb4b4b4,
Te6edf3PRECISION_ZOOM_MAX Te6edf3to Te6edf3constTb4b4b4(Te6edf3PRECISION_SCALE_MAXTb4b4b4)Tb4b4b4,
Tb4b4b4)
Te6edf3CircleLayerTb4b4b4(
Te6edf3id Tff7b72= Ta5d6ff"Ta5d6ffnode-precisionTa5d6ff"Tb4b4b4,
Te6edf3source Tff7b72= Te6edf3nodesSourceTb4b4b4,
Te6edf3filter Tff7b72= Tff7b72!Te6edf3featureTb4b4b4.Te6edf3hasTb4b4b4(Ta5d6ff"Ta5d6ffclusterTa5d6ff"Tb4b4b4)Tb4b4b4,
Te6edf3radius Tff7b72= Tb4b4b4(Te6edf3featureTff7b72[Ta5d6ff"Ta5d6ffprecision_metersTa5d6ff"Tff7b72]Tb4b4b4.Te6edf3convertToNumberTb4b4b4(Te6edf3constTb4b4b4(T79c0ff0fTb4b4b4)Tb4b4b4) Tff7b72* Te6edf3metersToPixelsTb4b4b4)Tb4b4b4.Te6edf3dpTb4b4b4,
Te6edf3color Tff7b72=
Te6edf3featureTff7b72[Ta5d6ff"Ta5d6ffbackground_colorTa5d6ff"Tff7b72]Tb4b4b4.Te6edf3convertToColorTb4b4b4(
Te6edf3constTb4b4b4(Te6edf3clusterColorTb4b4b4.Te6edf3copyTb4b4b4(Te6edf3alpha Tff7b72= Te6edf3PRECISION_CIRCLE_FILL_ALPHATb4b4b4)Tb4b4b4)Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Te6edf3opacity Tff7b72= Te6edf3constTb4b4b4(Te6edf3PRECISION_CIRCLE_FILL_ALPHATb4b4b4)Tb4b4b4,
Te6edf3strokeWidth Tff7b72= Te6edf3constTb4b4b4(T79c0ff1.Te6edf3dpTb4b4b4)Tb4b4b4,
Te6edf3strokeColor Tff7b72=
Te6edf3featureTff7b72[Ta5d6ff"Ta5d6ffbackground_colorTa5d6ff"Tff7b72]Tb4b4b4.Te6edf3convertToColorTb4b4b4(
Te6edf3constTb4b4b4(Te6edf3clusterColorTb4b4b4.Te6edf3copyTb4b4b4(Te6edf3alpha Tff7b72= Te6edf3PRECISION_CIRCLE_STROKE_ALPHATb4b4b4)Tb4b4b4)Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Te6edf3strokeOpacity Tff7b72= Te6edf3constTb4b4b4(Te6edf3PRECISION_CIRCLE_STROKE_ALPHATb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
T8b949e/** Waypoint markers rendered as symbol layer with emoji icons and click handling. */
Tf0883e@Composable
Tff7b72private Tff7b72fun Td2a8ffWaypointMarkerLayersTb4b4b4(Te6edf3waypointsTb4b4b4: Te6edf3MapTff7b72<Tffa657IntTb4b4b4, Te6edf3DataPacketTff7b72>Tb4b4b4, Te6edf3onWaypointClickTb4b4b4: Tb4b4b4(Tffa657IntTb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3featureCollection Tff7b72= Te6edf3rememberTb4b4b4(Te6edf3waypointsTb4b4b4) Tb4b4b4{ Te6edf3waypointsToFeatureCollectionTb4b4b4(Te6edf3waypointsTb4b4b4) Tb4b4b4}
Tff7b72val Te6edf3waypointSource Tff7b72= Te6edf3rememberGeoJsonSourceTb4b4b4(Te6edf3data Tff7b72= Te6edf3GeoJsonDataTb4b4b4.Te6edf3FeaturesTb4b4b4(Te6edf3featureCollectionTb4b4b4)Tb4b4b4)
T8b949e// Waypoint emoji labels
Te6edf3SymbolLayerTb4b4b4(
Te6edf3id Tff7b72= Ta5d6ff"Ta5d6ffwaypoint-markersTa5d6ff"Tb4b4b4,
Te6edf3source Tff7b72= Te6edf3waypointSourceTb4b4b4,
Te6edf3textField Tff7b72= Te6edf3featureTff7b72[Ta5d6ff"Ta5d6ffemojiTa5d6ff"Tff7b72]Tb4b4b4.Te6edf3asStringTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3textSize Tff7b72= Te6edf3constTb4b4b4(T79c0ff2fTb4b4b4.Te6edf3emTb4b4b4)Tb4b4b4,
Te6edf3textAllowOverlap Tff7b72= Te6edf3constTb4b4b4(Tff7b72trueTb4b4b4)Tb4b4b4,
Te6edf3iconAllowOverlap Tff7b72= Te6edf3constTb4b4b4(Tff7b72trueTb4b4b4)Tb4b4b4,
Te6edf3onClick Tff7b72= Tb4b4b4{ Te6edf3features Tff7b72-Tff7b72>
Tff7b72val Te6edf3waypointId Tff7b72= Te6edf3featuresTb4b4b4.Te6edf3firstOrNullTb4b4b4(Tb4b4b4)Tff7b72?.Te6edf3propertiesTff7b72?.Te6edf3getTb4b4b4(Ta5d6ff"Ta5d6ffwaypoint_idTa5d6ff"Tb4b4b4)Tff7b72?.Te6edf3toStringTb4b4b4(Tb4b4b4)Tff7b72?.Te6edf3toIntOrNullTb4b4b4(Tb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3waypointId Tff7b72!Tff7b72= Tff7b72nullTb4b4b4) Tb4b4b4{
Te6edf3onWaypointClickTb4b4b4(Te6edf3waypointIdTb4b4b4)
Te6edf3ClickResultTb4b4b4.Te6edf3Consume
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3ClickResultTb4b4b4.Te6edf3Pass
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Tb4b4b4)
T8b949e// Waypoint name labels below emoji
Te6edf3SymbolLayerTb4b4b4(
Te6edf3id Tff7b72= Ta5d6ff"Ta5d6ffwaypoint-labelsTa5d6ff"Tb4b4b4,
Te6edf3source Tff7b72= Te6edf3waypointSourceTb4b4b4,
Te6edf3textField Tff7b72= Te6edf3featureTff7b72[Ta5d6ff"Ta5d6ffnameTa5d6ff"Tff7b72]Tb4b4b4.Te6edf3asStringTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3textSize Tff7b72= Te6edf3constTb4b4b4(T79c0ff1.Te6edf3emTb4b4b4)Tb4b4b4,
Te6edf3textOffset Tff7b72= Te6edf3offsetTb4b4b4(T79c0ff0fTb4b4b4.Te6edf3emTb4b4b4, T79c0ff2fTb4b4b4.Te6edf3emTb4b4b4)Tb4b4b4,
Te6edf3textColor Tff7b72= Te6edf3constTb4b4b4(Te6edf3ColorTb4b4b4.Te6edf3DarkGrayTb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Served by rngit 1.5.0 - Generated in 0.14s